﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.ServiceModel;
using System.Threading.Tasks;
using System.Timers;
using Sern.Abstracts.Common;
using Sern.Abstracts.Tracker.DataContracts;
using Sern.Abstracts.Tracker.ServiceContracts;
using Sern.Common;
using Microsoft.Practices.Unity;
using Sern.ServiceImplementation.TrackerDefinition.Data;

namespace Sern.ServiceImplementation.TrackerDefinition.Service
{
    public partial class Tracker : ITrackerTelemetryReceiver
    {
        private static readonly Timer JobPollTimer;        
        private static readonly Random Rand = new Random();

        static Tracker()
        {
            JobPollTimer = new Timer(Constant.JobEndedPollingInterval.TotalMilliseconds);

            IObservable<bool> jobEndPulse = Observable.FromEventPattern<ElapsedEventHandler, ElapsedEventArgs>(
                handle => JobPollTimer.Elapsed += handle, handle => JobPollTimer.Elapsed -= handle)
                .Select(el =>
                    {
                        //Randomize is used to minize chance of a deadlock.
                        if(Constant.IsPollingRandomized)
                            System.Threading.Thread.Sleep(Rand.Next(Constant.JobEndedPollingInterval.Milliseconds));
                        return true;
                    } );

            jobEndPulse.Subscribe(
                Observer.Create<bool>(
                    signal =>
                        {
                            var endingJobs = TrackerCommon.GetJobsAboutToEnd().OrderBy(j => Guid.NewGuid()).ToArray();

                            foreach (var job in endingJobs)
                            {
                                var jobRepository = ComponentLocator.Unity.Resolve<IRepository<Job>>();
                                var jobCopy = jobRepository.Records.Single(r => r.Id == job.Id);
                                jobCopy.EndTime = DateTime.UtcNow;
                                jobRepository.Save();
                                NotifyJobEndingToAllClients(job.Id);
                            }
                        }));
        }

        /// <summary>
        /// Notify the clienthost service that a client it is hosting is ending the subscribtion (usually after the tracker gets an end notification from the feeder)
        /// -- The tracker will then wait until all pending task is complete before removing subscription.
        /// </summary>
        /// <param name="jobId"></param>
        private async static void NotifyJobEndingToAllClients(Guid jobId)
        {
            var clientRepository = ComponentLocator.Unity.Resolve<IRepository<Client>>();
            var endingClients = clientRepository.Records
                .OfType<ClientStatusInfo>().Where(c => c.JobId == jobId).ToArray();


            Parallel.ForEach(endingClients, client =>
                {
                    client.EndOfSubscription = DateTime.UtcNow;
                    var hostBinding = ComponentLocator.Unity.Resolve<IBindingFactory>()[(ClientType) client.Type]();

                    try
                    {
                        using (
                            var clientHost =
                                new ChannelFactory<IMapReduceHost>(hostBinding.Binding, client.ClientHostUri).
                                    CreateChannel()
                                    .Wrap())
                        {
                            clientHost.Proxy.Deregister(client.Id);
                        }
                    }catch
                    {
                        //TODO: Should log when a client cannot be ended. possibly as a warning, not necessarily an error, since the job is over anyways.
                    }
                });

            clientRepository.Save();
        }



        /// <summary>
        /// Notify the tracker that the client is working and what state of completion it's in.
        /// </summary>
        /// <param name="notice"></param>
        [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)]
        public void Notify(ProcessingNotice notice)
        {
            //Check if the key node exists, if not inserts a new one. If the key has changed should actually throw an exception.
            KeyTree existingKey = null;

            //Must succeed getting existing key or adding a new one before it can escape this loop.
            do
            {
                try
                {                    
                    existingKey = AddOrGetExistingKey(notice);
                }
                catch (Exception ex){}
            } while (existingKey == null);


            var keyAtClientLogRepository = ComponentLocator.Unity.Resolve<IRepository<KeyAtClientLog>>();
            keyAtClientLogRepository.Add(
                                            new KeyAtClientLog
                                             {
                                                 ClientId = notice.ClientId,
                                                 Id = Guid.NewGuid(),
                                                 KeyId = existingKey.Id,
                                                 State = (int)notice.ProcessingStage
                                             });

            keyAtClientLogRepository.Save();

            if (notice.ProcessingStage == ProcessingStage.Completed)
            {
                //Get the 2 clients involved in the key processing. The initiator and the completer.
                var clientAndLogs =
                    (from client in ComponentLocator.Unity.Resolve<IRepository<Client>>().Records
                    join keyLog in
                        ComponentLocator.Unity.Resolve<IRepository<KeyAtClientLog>>().Records.Where(
                            _ => _.KeyId == notice.Key)
                        on client.Id equals keyLog.ClientId
                    select new {Client = client, Log = keyLog}).ToArray();

                var initiatingClient =
                    clientAndLogs
                    .Where(_ => _.Log.State == (int) ProcessingStage.Initiated).Select(_ => _.Client)
                    .Single();

                var endingClient =
                    clientAndLogs
                    .Where(_ => _.Log.State == (int)ProcessingStage.Completed).Select(_ => _.Client)
                    .Single();

                //Notify all the monitors that a job was completed.
                Parallel.ForEach(
                    ComponentLocator.Unity.Resolve<IEnumerable<IClient<ITrackerMonitoringClient>>>(),
                    _ => _.Proxy.Notify(
                            new EmissionNotice
                                {
                                    NextKey = notice.Key,
                                    PreviousKey = notice.ParentKey,
                                    FromClient = new ClientInfo
                                                     {
                                                         JobId = initiatingClient.JobId,
                                                         Id = initiatingClient.Id,
                                                         KeyShape = initiatingClient.KeyShape,
                                                         ValueShape = initiatingClient.ValueShape,
                                                         Uri = initiatingClient.Uri,
                                                         Type = (ClientType) initiatingClient.Type,
                                                         HostUri = initiatingClient.ClientHostUri
                                                     },
                                    ToClient = new ClientInfo
                                                   {
                                                       JobId = endingClient.JobId,
                                                       Id = endingClient.Id,
                                                       KeyShape = endingClient.KeyShape,
                                                       ValueShape = endingClient.ValueShape,
                                                       Uri = endingClient.Uri,
                                                       Type = (ClientType) endingClient.Type,
                                                       HostUri = endingClient.ClientHostUri
                                                   }
                                  
                                }
                        )
                );
            }
        }

        /// <summary>
        /// Add or get existing key to the KeyTree.
        /// </summary>
        /// <param name="notice"></param>
        /// <returns></returns>
        private static KeyTree AddOrGetExistingKey(ProcessingNotice notice)
        {
            KeyTree existingKey;
            var keyTreeRepository = ComponentLocator.Unity.Resolve<IRepository<KeyTree>>();

            existingKey = (from key in keyTreeRepository.Records
                           where key.Id == notice.Key
                           select key).SingleOrDefault();
            if (existingKey == null)
            {
                existingKey = new KeyTree
                                  {
                                      Id = notice.Key,
                                      ParentKey = notice.ParentKey,
                                  };
                keyTreeRepository.Add(existingKey);
                keyTreeRepository.Save();
            }
            else if(existingKey.ParentKey != notice.ParentKey)
            {
                existingKey.ParentKey = notice.ParentKey;
            }
            return existingKey;
        }

        /// <summary>
        /// Notify the last ping time.
        /// </summary>
        /// <param name="notice"></param>
        /// <remarks>Should we allow a dead client to rejoin? Right now this isn't the case, because we're looking out for expired session.</remarks>
        [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)]
        public void Notify(IsAliveNotice notice)
        {
            var clientStatusRepository = ComponentLocator.Unity.Resolve<IRepository<Client>>();

            //Expired client cannot be updated ie. that's any client that has ended or lost contact with.
            var clientStatus = (from _ in clientStatusRepository.Records.OfType<ClientStatusInfo>()
                                where _.Id == notice.ClientId
                                      && _.EndOfSubscription == null
                                      && (_.StartOfSubscription >= TrackerCommon.LastValidActivityTime
                                          || _.LastAlivePing >= TrackerCommon.LastValidActivityTime)
                                select _).SingleOrDefault();

            if (clientStatus != null)
            {
                clientStatus.LastAlivePing = DateTime.UtcNow;
                clientStatusRepository.Save();
            }
        }

        /// <summary>
        /// Notify that feeding is over, that once every KvP spawned off that feeder has finished running, we can complete the job.
        /// </summary>
        /// <param name="jobId"></param>
        [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)]
        public void NotifyEnd(Guid jobId)
        {
            var endingJob = ComponentLocator.Unity.Resolve<IRepository<Job>>().Records
                .Where(j => j.Id == jobId && j.EndingTime == null).Single();

            endingJob.EndingTime = DateTime.UtcNow;
            //TODO: Join up and find if there's any initialized key with no completion date. Check the max of them and add the the timeout to it.
            //TODO: At the end of the time out check to see if the max timeout is pushed further back, if so continue to wait until max timeout stops changing.
            //TODO: Then query all the map reduce clients, and call an end, so each of these clients can reset!
            //TODO: Tracker needs to be stateless, so it can easily be load balanced. Consider requiring jobId to be passed on every call.
            //TODO: Tracker host will not physically register the address then only adds it to the repository so we can have fail over.
        }
    }
}
